﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using Framework;
using UnityEngine;
using Debug = UnityEngine.Debug;

namespace FrameworkEditor
{
    /// <summary>
    /// </summary>
    internal class CompileModuleInfo
    {
        public string moduleName;
        public bool compile32Bit;
        public bool compile64Bit;
        public readonly List<string> fileList = new List<string>();
    }

    /// <summary>
    /// </summary>
    public static class CompileLuaCodeV2
    {
        /// <summary>
        ///     tolua的核心lua代码路径
        /// </summary>
        private static readonly string _toluaDir = FrameworkSettings.FrameworkToLuaBaseLuaDir;

        /// <summary>
        ///     lua的启动部分逻辑代码路径，负责热更新的
        /// </summary>
        private static readonly string _luaFrameworkBooterDir = FrameworkSettings.FrameworkLuaRootDir + "/framework";

        /// <summary>
        ///     框架中的启动代码
        /// </summary>
        private static readonly string _luaBooterDir = FrameworkSettings.FrameworkLuaRootDir + "/booter";

        /// <summary>
        ///     项目中定制的启动代码
        /// </summary>
        private static readonly string _luaProjBooterDir = FrameworkSettings.ProjLuaRootDir + "/projbooter";

        /// <summary>
        ///     lua具体业务逻辑代码路径
        /// </summary>
        private static readonly string _luaModulesDir = FrameworkSettings.ProjLuaRootDir + "/modules";

        /// <summary>
        ///     收集到的lua列表存储在该文件中
        /// </summary>
        private const string ListFileName = "file_list.txt";

        /// <summary>
        ///     按照上述文件夹定义，每个文件夹单独编译成字节码，并合并到单个文件
        /// </summary>
        /// <param name="outResRootPath">外部资源库路径</param>
        /// <param name="targetPlatform">目标平台</param>
        /// <param name="isFromCi"></param>
        /// <exception cref="ArgumentException">暂时不支持OSXPlayer为targetPlatform的lua编译</exception>
        public static int CompileLuaToBytes(string outResRootPath, int targetPlatform, bool isFromCi = false)
        {
            var finalOutputDir = outResRootPath + "/" + FrameworkSettings.OutLuaBytesDirName.ToLower();

            var (toolName32, toolRunDir32, toolArgs32) = GetToolNameAndDir(targetPlatform, true);
            var (toolName64, toolRunDir64, toolArgs64) = GetToolNameAndDir(targetPlatform, false);

            // 用于加速创建目录的缓存
            var createdDirCache = new HashSet<string>();

            // 定义要收集的模块
            var compileModuleInfoList = new List<CompileModuleInfo>();
            {
                var stopwatch = new Stopwatch();
                stopwatch.Start();
                //启动相关
                compileModuleInfoList.Add(GenerateCompileModuleInfo(targetPlatform, _toluaDir, "tolua", true, createdDirCache));
                compileModuleInfoList.Add(GenerateCompileModuleInfo(targetPlatform, _luaBooterDir, "booter", false, createdDirCache));
                compileModuleInfoList.Add(GenerateCompileModuleInfo(targetPlatform, _luaProjBooterDir, "projbooter", false, createdDirCache));
                //启动之后的逻辑
                compileModuleInfoList.Add(GenerateCompileModuleInfo(targetPlatform, _luaFrameworkBooterDir, "framework", false, createdDirCache));
                compileModuleInfoList.Add(GenerateCompileModuleInfo(targetPlatform, _luaModulesDir, "modules", false, createdDirCache));
                stopwatch.Stop();
                Debug.Log($"build time elapse for lua GenerateCompileModuleInfo： {stopwatch.Elapsed.TotalSeconds}s!");
            }

            // generate file list from all module
            var compileFileList32 = new List<string>(20000);
            var compileFileList64 = new List<string>(20000);
            foreach (var moduleInfo in compileModuleInfoList)
            {
                if (moduleInfo.compile32Bit)
                {
                    compileFileList32.AddRange(moduleInfo.fileList);
                }

                if (moduleInfo.compile64Bit)
                {
                    compileFileList64.AddRange(moduleInfo.fileList);
                }
            }

            // 生成 32bit 版本的 lua bytes
            {
                var stopwatch = new Stopwatch();
                stopwatch.Start();
                if (compileFileList32.Count > 0)
                {
                    // generate file list
                    File.WriteAllLines(Path.Combine(toolRunDir32, ListFileName), compileFileList32);

                    var exitCode = RunLuaTool(toolName32, $"{toolArgs32} {ListFileName} {FrameworkSettings.LuaCodeExt}", toolRunDir32, isFromCi);
                    if (exitCode != 0)
                    {
                        return exitCode;
                    }
                }

                stopwatch.Stop();
                Debug.Log($"build time elapse for lua compile 32bit bytecode： {stopwatch.Elapsed.TotalSeconds}s!");
            }

            // 生成 64bit 版本的 lua bytes
            {
                var stopwatch = new Stopwatch();
                stopwatch.Start();
                if (compileFileList64.Count > 0)
                {
                    if (targetPlatform == FrameworkSettings.OSXPlayer)
                    {
                        throw new ArgumentException("not support OSXPlayer target platform yet");
                    }

                    // generate file list
                    File.WriteAllLines(Path.Combine(toolRunDir64, ListFileName), compileFileList64);
                    var exitCode = RunLuaTool(toolName64, $"{toolArgs64} {ListFileName} {FrameworkSettings.LuaCodeExt}", toolRunDir64, isFromCi);
                    if (exitCode != 0)
                    {
                        return exitCode;
                    }
                }

                stopwatch.Stop();
                Debug.Log($"build time elapse for lua compile 64bit bytecode： {stopwatch.Elapsed.TotalSeconds}s!");
            }

            // 压缩并最终输出dat文件
            {
                var stopwatch = new Stopwatch();
                stopwatch.Start();
                var outRootDir = FileHelper.GetUnityPath(finalOutputDir);
                foreach (var moduleInfo in compileModuleInfoList)
                {
                    if (moduleInfo.compile32Bit)
                    {
                        var md5CollectName32 = MD5Util.ComputeMd5($"{moduleInfo.moduleName}_32");
                        var collectFilePath32 = outRootDir + "/" + md5CollectName32 + FrameworkSettings.LuaCodeExt;
                        CompressLuaByteCode(moduleInfo, toolRunDir32, collectFilePath32);
                        CheckLuaCode(collectFilePath32, moduleInfo, isFromCi);
                    }

                    if (moduleInfo.compile64Bit)
                    {
                        var md5CollectName64 = MD5Util.ComputeMd5($"{moduleInfo.moduleName}_64");
                        var collectFilePath64 = outRootDir + "/" + md5CollectName64 + FrameworkSettings.LuaCodeExt;
                        CompressLuaByteCode(moduleInfo, toolRunDir64, collectFilePath64);
                        CheckLuaCode(collectFilePath64, moduleInfo, isFromCi);
                    }
                }

                stopwatch.Stop();
                Debug.Log($"build time elapse for lua compress byte code： {stopwatch.Elapsed.TotalSeconds}s!");
            }

            return 0;
        }

        /// <summary>
        ///     生成编译时用的命令行参数信息
        /// </summary>
        /// <param name="targetPlatform"></param>
        /// <param name="is32Bit"></param>
        /// <returns>(toolName: 可执行命令行文件名, toolRunDir: 可执行命令行文件路径, toolArgs: 命令行参数)</returns>
        private static (string, string, string) GetToolNameAndDir(int targetPlatform, bool is32Bit)
        {
            string toolName;
            string toolRunDir;
            string toolArgs;

            // mac standalone单独用一个工具, luac5.1.5
            // windows，android，ios都可以用luajit来编译
            // windows tool，只能打windows和android
            if (FrameworkSettings.CurPlatform == FrameworkSettings.WindowsPlayer)
            {
                if (targetPlatform != FrameworkSettings.AndroidPlayer)
                {
                    toolName = "luajit64.exe";
                    toolRunDir = Application.dataPath.Replace("Assets", "luajit64");
                }
                else
                {
                    toolName = is32Bit ? "luajit32.exe" : "luajit64.exe";
                    toolRunDir = Application.dataPath.Replace("Assets", is32Bit ? "luajit" : "luajit64");
                }
            }
            // mac编辑器打包
            else
            {
                // 目标是mac电脑，则使用luac
                if (targetPlatform == FrameworkSettings.OSXPlayer)
                {
                    toolName = "./luac";
                    toolRunDir = Application.dataPath.Replace("Assets", "osxlua");
                }
                // android 要分开32和64
                else if (targetPlatform == FrameworkSettings.AndroidPlayer)
                {
                    toolName = "./luajit";
                    toolRunDir = Application.dataPath.Replace("Assets", is32Bit ? "osxlua/32" : "osxlua/64");
                }
                // iOS统一用64
                else
                {
                    toolName = "./luajit";
                    toolRunDir = Application.dataPath.Replace("Assets", "osxlua/64");
                }
            }

            if (targetPlatform != FrameworkSettings.OSXPlayer)
            {
                // if (CiBuilder.IsDebugBuild())
                if (false)
                {
                    toolArgs = "-b -g -f";
                }
                else
                {
                    toolArgs = "-b -f";
                }
            }
            else
            {
                toolArgs = "-o";
            }

            return (toolName, toolRunDir, toolArgs);
        }

        /// <summary>
        ///     生成模块信息
        /// </summary>
        /// <param name="targetPlatform">目标平台</param>
        /// <param name="dirPath">lua源码</param>
        /// <param name="moduleName">模块名</param>
        /// <param name="isTolua">true表示是tolua核心的，路径保持；我们自定义的保留一级目录</param>
        /// <param name="createdDirCache">存储已经创建过的文件夹,用于加速判断</param>
        private static CompileModuleInfo GenerateCompileModuleInfo(int targetPlatform,
                                                                   string dirPath,
                                                                   string moduleName,
                                                                   bool isTolua,
                                                                   HashSet<string> createdDirCache)
        {
            dirPath = FileHelper.GetUnityPath(dirPath);
            if (!Directory.Exists(dirPath))
            {
                Debug.LogError("CompileOneDir dirPath = " + dirPath + " does not exists!");
                return null;
            }

            var compileModuleInfo = new CompileModuleInfo
            {
                moduleName = moduleName,
                // android编译32和64，其余的都只编译64
                // mac os x 不能运行32位lua编译器
                compile32Bit = FrameworkSettings.AndroidPlayer == targetPlatform && FrameworkSettings.CurPlatform == FrameworkSettings.WindowsPlayer,
                compile64Bit = true
            };

            var (_, toolRunDir32, _) = GetToolNameAndDir(targetPlatform, true);
            var (_, toolRunDir64, _) = GetToolNameAndDir(targetPlatform, false);

            var dirName = Path.GetFileName(dirPath);
            foreach (var file in Directory.GetFiles(dirPath, "*.lua", SearchOption.AllDirectories))
            {
                var filePath = FileHelper.GetUnityPath(file);
                var relativePath = EditorUtils.GetRelativePathToDir(filePath, dirPath);
                if (!isTolua)
                {
                    relativePath = dirName + "/" + relativePath;
                }

                if (compileModuleInfo.compile32Bit)
                {
                    CopyFileForCompile(filePath, relativePath, toolRunDir32, createdDirCache);
                }

                if (compileModuleInfo.compile64Bit)
                {
                    CopyFileForCompile(filePath, relativePath, toolRunDir64, createdDirCache);
                }

                if (compileModuleInfo.compile32Bit || compileModuleInfo.compile64Bit)
                {
                    compileModuleInfo.fileList.Add(relativePath);
                }
            }

            return compileModuleInfo;
        }

        /// <summary>
        ///     编译一个lua文件
        /// </summary>
        /// <param name="luaFilePath">lua文件路径</param>
        /// <param name="relativePath">lua文件相对路径</param>
        /// <param name="luaRootDir">lua文件夹根目录</param>
        /// <param name="createdDirCache">存储已经创建过的文件夹,用于加速判断</param>
        private static void CopyFileForCompile(string luaFilePath, string relativePath, string luaRootDir, HashSet<string> createdDirCache)
        {
            var copyToPath = luaRootDir + "/" + relativePath;
            if (!createdDirCache.Contains(copyToPath))
            {
                FileHelper.EnsureDirForFile(copyToPath);
                createdDirCache.Add(copyToPath);
            }

            // we don't copy the jit/ folder under sframework, use luajitXX/jit instead
            if (!relativePath.StartsWith("jit/"))
            {
                File.Copy(luaFilePath, copyToPath, true);
            }
        }

        /// <summary>
        ///     合并同模块 bytecode 到同一个dat文件
        /// </summary>
        /// <param name="moduleInfo"></param>
        /// <param name="bytesRootDir"></param>
        /// <param name="resLibFilePath"></param>
        private static void CompressLuaByteCode(CompileModuleInfo moduleInfo, string bytesRootDir, string resLibFilePath)
        {
            FileHelper.EnsureDirForFile(resLibFilePath);
            FileHelper.DeleteFile(resLibFilePath);

            using (var moduleMemStream = new MemoryStream(16 * 1024 * 1024))
            {
                using (var binaryWriter = new BinaryWriter(moduleMemStream))
                {
                    foreach (var relativePath in moduleInfo.fileList)
                    {
                        var pathMd5 = MD5Util.ComputeMd5(relativePath);
                        // Debug.Log($"compress relativePath:{relativePath}, pathMd5:{pathMd5}");
                        var code = StringHelper.Str2ByteArray(pathMd5);
                        var absolutePath = FileHelper.GetUnityPath(Path.Combine(bytesRootDir, $"{relativePath}{FrameworkSettings.LuaCodeExt}"));
                        var data = FileHelper.ReadBytes(absolutePath);
                        data = UnityZipUtil.ZipBufferWithConstModifyTime(data);
                        // TODO: can be optimized by using SIMD
                        for (var idx = 0; idx < data.Length; idx++)
                        {
                            data[idx] = (byte)(code[idx % code.Length] ^ data[idx]);
                        }

                        binaryWriter.Write(pathMd5);
                        binaryWriter.Write(data.Length);
                        binaryWriter.Write(data);
                    }
                }

                var allData = EditorUtils.RsaSign(moduleMemStream.ToArray());
                FileHelper.WriteAllBytesToPath(resLibFilePath, allData);
            }
        }

        private static void CheckLuaCode(string resLibFilePath, CompileModuleInfo moduleInfo, bool isFromCi)
        {
            var allCodeData = FileHelper.ReadBytes(resLibFilePath);
            int dataCount;
            if (isFromCi)
            {
                dataCount = FileHelper.RsaJustVerityRunTime(allCodeData);
            }
            else
            {
                dataCount = FileHelper.RsaJustVerity(allCodeData);
            }

            var allLuaCode = new Dictionary<string, byte[]>();
            if (dataCount == 0)
            {
                throw new Exception("LuaCodeLoader.AddLuaCode, data rsa check failed!!");
            }

            using (var memoryStream = new MemoryStream(allCodeData, allCodeData.Length - dataCount, dataCount))
            {
                using (var binaryReader = new BinaryReader(memoryStream))
                {
                    memoryStream.Seek(0, SeekOrigin.Begin);
                    while (memoryStream.Position < memoryStream.Length)
                    {
                        var pathMd5 = binaryReader.ReadString();
                        var dataLength = binaryReader.ReadInt32();
                        var data = binaryReader.ReadBytes(dataLength);
                        allLuaCode[pathMd5] = data;
                    }
                }
            }

            foreach (var relativePath in moduleInfo.fileList)
            {
                if (relativePath.StartsWith("jit/"))
                {
                    continue;
                }

                var pathMd5 = MD5Util.ComputeMd5(relativePath);
                if (allLuaCode.ContainsKey(pathMd5) == false)
                {
                    throw new Exception("lua文件验证失败，缺少文件：" + relativePath);
                }

                if (allLuaCode[pathMd5].Length == 0)
                {
                    throw new Exception("lua文件验证失败，文件长度为0：" + relativePath);
                }
            }
        }

        /// <summary>
        ///     执行lua编译工具命令
        /// </summary>
        /// <param name="toolName">工具名字</param>
        /// <param name="toolArgs">工具参数</param>
        /// <param name="runDir">工具路径</param>
        /// <param name="isFromCi"></param>
        private static int RunLuaTool(string toolName, string toolArgs, string runDir, bool isFromCi)
        {
            var toolPath = Path.Combine(runDir, toolName);
            if (!FileHelper.IsFileExists(toolPath))
            {
                Debug.LogError("RunLuaTool, toolPath = " + toolPath + " is not exists!");
                return -1;
            }

            var procInfo = new ProcessStartInfo();
            procInfo.UseShellExecute = false;
            procInfo.Arguments = toolArgs;

            // ref: https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-diagnostics-processstartinfo-useshellexecute
            // * The WorkingDirectory property behaves differently depending on the value of the UseShellExecute property. When UseShellExecute is true, the
            // WorkingDirectory property specifies the location of the executable. If WorkingDirectory is an empty string, it's assumed that the current
            // directory contains the executable.
            // * UseShellExecute must be true if you set the ErrorDialog property to true.
            // * When UseShellExecute is false, the WorkingDirectory property is not used to find the executable. Instead, it is used only by the process that
            // is started and has meaning only within the context of the new process. When UseShellExecute is false, the FileName property can be either a fully
            // qualified path to the executable, or a simple executable name that the system will attempt to find within folders specified by the PATH
            // environment variable. The interpretation of the search path depends on the operating system. For more information, enter HELP PATH or man sh at a
            // command prompt.
            procInfo.FileName = $"{runDir}/{toolName}";
            procInfo.WindowStyle = ProcessWindowStyle.Hidden;
            procInfo.RedirectStandardError = true;
            procInfo.RedirectStandardOutput = true;

            // 注意: 这里必须设置工作目录, 确保 Windows, Macos, 编辑器环境 和 CI 环境下所有情况都能正常运行
            var workDir = Directory.GetCurrentDirectory();
            Directory.SetCurrentDirectory(runDir);

            var outputMsg = new StringBuilder();
            var errorMsg = new StringBuilder();
            Process proc;
            try
            {
                proc = Process.Start(procInfo);
                if (proc != null)
                {
                    // ref: https://learn.microsoft.com/zh-cn/dotnet/api/system.diagnostics.process.standarderror?view=netframework-4.8#system-diagnostics-process-standarderror
                    // To avoid deadlocks, use an asynchronous read operation on at least one of the streams.  
                    while (!proc.WaitForExit(1))
                    {
                        outputMsg.Append(proc.StandardOutput.ReadToEnd());
                        errorMsg.Append(proc.StandardError.ReadToEnd());
                    }
                }
            }
            catch (Exception e)
            {
                Debug.LogError($"RunLuaTool, Process.Start failed, {e}");
                return -1;
            }
            finally
            {
                Directory.SetCurrentDirectory(workDir);
            }

            var errorCode = 0;
            if (proc != null)
            {
                errorCode = proc.ExitCode;
                proc.Close();
            }

            if (outputMsg.Length > 0)
            {
                Debug.Log(outputMsg);
            }

            if (errorCode != 0)
            {
                Debug.LogError($"compile lua failed with exit code={errorCode}, errorMsg={errorMsg}");
            }

            return errorCode;
        }
    }
}